RecyclerView 性能优化:把加载表项耗时减半!
转自:掘金 唐子玄 https://juejin.cn/post/6939666015500369950
构建 Android App 界面时,RecyclerView 出场率很高。它的加载性能影响着用户体检。本篇分享一次完整的 RecyclerView 性能优化过程:从用工具定位问题,再不断尝试各种优化方案,最终达成 50% 的性能优化。
这次性能调优的界面如下:
预优化,先量化
这个排行榜嵌套在一个 ViewPager 中。最初发现性能问题是因为滑动到该界面时,ViewPager 指示器的平移动画卡了一下,掉帧了。
虽然卡顿是肉眼可见的,但若不能量化卡顿,就无法量化优化程度。
第一个想到的工具是GPU 呈现模式分析
。开启它的路径如下:打开手机设置 --- 开发者选项 --- GPU 呈现模式分析 --- 在屏幕上显示为条形图:
开启后,绘制性能就会图形化展示如下:
虽然图形化很直观,但量化地还不够细致,绘制耗时最好能精确到毫秒。所以转战到另一种方式“在 adb shell dumpsys gfxinfo 中”。选中后,打开排行榜界面,然后输入命令adb shell dumpsys gfxinfo <包名>
,最近 n 针的渲染时长就会罗列如下:
Draw Prepare Process Execute
50.00 0.23 6.82 1.28
50.00 0.26 1.49 1.13
7.01 0.24 1.58 0.76
6.41 0.52 7.42 1.34
13.13 0.18 2.01 0.76
4.38 0.15 1.72 0.39
4.37 0.15 1.13 0.33
4.36 0.15 1.23 0.38
4.34 0.35 1.15 0.31
4.36 0.15 1.16 8.42
4.32 0.14 1.11 0.31
4.32 0.15 1.10 0.32
每一行代表一帧渲染中各个阶段的耗时。
用另一个命令还可以得到更加精确的数据:adb shell dumpsys gfxinfo <包名> framestats
,该命令会从应用生成的最近 120 个帧中输出带有纳秒时间戳的帧时间信息:
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
1,299873182486990,299873215820322,9223372036854775807,0,299873227699771,299873227750761,299873228134563,299873242278000,299873243236959,299873243432011,299873243482063,299873244517375,299873245505396,62000,670000,
0,299873232346485,299873249013151,9223372036854775807,0,299873253133625,299873253191177,299873253443990,299873418812375,299873433404406,299873433753313,299873434022167,299873435099667,299873435955448,71000,453000,
0,299873448312229,299873448312229,9223372036854775807,0,299873448760344,299873448798573,299873449290656,299873449438469,299873449500969,299873449733261,299873449909979,299873450770344,299873451478625,65000,264000,
0,299873464924749,299873464924749,9223372036854775807,0,299873465493625,299873465550292,299873466377896,299873466594511,299873466643417,299873466932115,299873468812427,299873475972792,299873476852011,145000,198000,
0,299873481537390,299873481537390,9223372036854775807,0,299873481932896,299873481972688,299873482590188,299873482741333,299873482772688,299873483047375,299873483263886,299873483856958,299873484308886,72000,96000,
0,299873498151975,299873498151975,9223372036854775807,0,299873498582427,299873498633417,299873499218833,299873499442271,299873499483781,299873499850761,299873500327427,299873501493886,299873502170708,126000,186000,
0,299873514764506,299873514764506,9223372036854775807,0,299873515260031,299873515314667,299873515966646,299873516205656,299873516254667,299873516538052,299873516896229,299873518042063,299873518653990,141000,149000,
0,299873531376920,299873531376920,9223372036854775807,0,299873531891646,299873531951906,299873532798261,299873533022792,299873533072219,299873533390292,299873533748886,299873534813729,299873535444771,118000,160000,
0,299873547989162,299873547989162,9223372036854775807,0,299873548571750,299873548638990,299873549306594,299873549534042,299873549590969,299873549916958,299873550342167,299873551852063,299873552613886,128000,200000,
0,299873564601513,299873564601513,9223372036854775807,0,299873565070500,299873565126281,299873565700031,299873565924302,299873565977479,299873566281073,299873566635865,299873567892375,299873568616386,143000,167000,
0,299873581213965,299873581213965,9223372036854775807,0,299873581577948,299873581621073,299873583047271,299873583253833,299873583293729,299873583680031,299873584854875,299873592375136,299873593527688,222000,308000,
原生输出信息没有可读性,但它们遵守 csv 格式,复制粘贴到 wps 表格中,选中 数据 --- 分列,用“逗号”分割:
数据就以表格的形式展示:
每一行表示一帧绘制的时间信息,一共有 16 列,每一列表示一个关键节点的时间戳,比如PerformTraversalsStart
表示绘制遍历的开始时间点,DrawStart
表示onDraw()
开始的时间点,前者减去后者表示measure + layout
的耗时。
利用表格的求差功能可以计算出一排表征性能的耗时。
虽然得到了量化数据,但是这么折腾着实有点辛苦。
一顿搜索之后,终于找到了下面这个高效的方法:
class TestActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window?.addOnFrameMetricsAvailableListener(Window.OnFrameMetricsAvailableListener { window, frameMetrics, dropCountSinceLastInvocation ->
Log.v("test","measure + layout=${frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)/1000000}, " +
" delay=${frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)/1000000}, " +
" anim=${frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)/1000000}," +
" touch=${frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)/1000000}, " +
" draw=${frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)/1000000}, " +
" total=${frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)/1000000}")
}, Handler())
}
}
Window.addOnFrameMetricsAvailableListener()
方法可以监听最近 120 帧的绘制耗时,它的数据源和上面 adb 命令是一样的。
我把自己感兴趣的耗时都打印了出来,分别是 measure + layout、延迟、动画、触摸、绘制、总耗时。
然后打开了排行榜界面,得到了如下数据:
measure + layout=370, delay=35, anim=0, touch=0, draw=21, total=435
measure + layout=0, delay=451, anim=3, touch=0, draw=0, total=467
measure + layout=22, delay=6, anim=0, touch=0, draw=3, total=34
measure + layout=0, delay=17, anim=0, touch=0, draw=0, total=41
有一帧绘制耗时高达 435 ms,其中 measure + layout 占了 370 ms。(此数值在不同手机上差异较大)
然后我关闭了 log 过滤,发现了更多信息:
measure + layout=370, delay=35, anim=0, touch=0, draw=21, total=435
Skipped 23 frames! The application may be doing too much work on its main thread.
measure + layout=0, delay=451, anim=3, touch=0, draw=0, total=467
measure + layout=22, delay=6, anim=0, touch=0, draw=3, total=34
measure + layout=0, delay=17, anim=0, touch=0, draw=0, total=41
紧接着耗时最长的那一帧,有一条警告,它是由Choreographer
打印的,表示此刻发生掉帧,而且掉了整整 23 帧。。。(关于 Choreographer 详细的源码解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?)
动态构建布局,弃用 xml
首先想到的一个方案是:“弃用 xml”
onCreateViewHolder()
执行在主线程,如果它执行耗时,势必会影响到也运行在主线程的绘制性能。
demo 中排行榜一共有两类 item:表头和表体,其中构建表头布局的代码如下:
// 表头 item 代理类, 描述如何构建 item 及为它绑定数据
class HeaderProxy:VarietyAdapter.Proxy<Header, HeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// 构建 item
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.header_layout,parent,false)
return HeaderProxy(itemView)
}
override fun onBindViewHolder(holder: HeaderViewHolder, data: Header, index: Int, action: ((Any?) -> Unit)?) {
// 为 item 绑定数据
holder.tvCount?.text = data.count
holder.tvName?.text = data.name
holder.tvRank?.text = data.rank
holder.tvTitle?.text = data.title
}
}
// 表头实体类
data class Header(
val rank: String,// 排名
val name: String,// 主播
val count: String,// 粉丝数
val title: String// 表头
)
// 用于保存表项控件引用的 ViewHolder
class HeaderViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
val tvCount = itemView.findViewById<TextView>(R.id.tvCount)
val tvName = itemView.findViewById<TextView>(R.id.tvName)
val tvRank = itemView.findViewById<TextView>(R.id.tvRank)
val tvTitle = itemView.findViewById<TextView>(R.id.tvtitle)
}
原本这些逻辑应该写在RecyclerView.Adapter
中,把它单独抽象到一个 proxy 类中,是为了解耦,以便更容易地为列表添加不同类型的表项:
private val adapter = VarietyAdapter().apply {
addProxy(HeaderProxy())
addProxy(RankProxy())
}
调用addProxy()
就动态地添加一种新表项类型(关于代理模式的实战应用可以点击代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂)。
在onCreateViewHolder()
中通过解析布局文件的方式来构建表项 item。
但解析布局文件需要进行 IO 操作将布局文件读到内存中,再解析 xml 根据标签 new 出对应的控件实例,最后 addView() 到容器中。这个过程是耗时的。
如果能使用 kotlin 代码直接完成布局的构建,则可以加速这个过程。但这样的构建代码可读性很差,后期想要更改控件的某个属性很难定位。
利用 kotlin 的DSL
来改善构建代码的可读性,甚至超越 xml:
class HeaderProxy : VarietyAdapter.Proxy<Header, HeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val itemView = parent.context.run {
LinearLayout { // 构建 LinearLayout
layout_width = match_parent
layout_height = wrap_content
orientation = vertical
padding_top = 10
padding_horizontal = 10
shape = shape {
corner_radii = intArrayOf(20, 20, 20, 20, 0, 0, 0, 0)
solid_color = "#ffffff"
}
TextView { // 构建 TextView
layout_id = "tvTitle"
layout_width = wrap_content
layout_height = wrap_content
textSize = 16f
textColor = "#3F4658"
textStyle = bold
margin_bottom = 3
}
ConstraintLayout { // 构建 ConstraintLayout
layout_width = match_parent
layout_height = wrap_content
margin_top = 16
TextView { // 构建 TextView
layout_id = "tvRank"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
start_toStartOf = parent_id
center_vertical = true
}
TextView { // 构建 TextView
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
align_vertical_to = "tvRank"
start_toEndOf = "tvRank"
margin_start = 19
}
TextView { // 构建 TextView
layout_id = "tvCount"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#9DA4AD"
align_vertical_to = "tvRank"
end_toEndOf = parent_id
}
}
}
}
return HeaderViewHolder(itemView)
}
override fun onBindViewHolder(holder: HeaderViewHolder, data: Header, index: Int, action: ((Any?) -> Unit)?) {
holder.tvCount?.text = data.count
holder.tvName?.text = data.name
holder.tvRank?.text = data.rank
holder.tvTitle?.text = data.title
}
}
// 表头实体类
data class Header(
val rank: String,// 排名
val name: String,// 主播
val count: String,// 粉丝数
val title: String// 表头
)
// 用于记录表项控件引用的 ViewHolder
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvRank = itemView.find<TextView>("tvRank")
val tvName = itemView.find<TextView>("tvName")
val tvCount = itemView.find<TextView>("tvCount")
val tvTitle = itemView.find<TextView>("tvTitle")
}
关于如何使用 DSL 简化布局构建可以点击Android性能优化 | 把构建布局用时缩短 20 倍(下)
将表头和表体 item 都用DSL
重构了一番,运行 demo 看看数据:
measure + layout=330, delay=47, anim=0, touch=0, draw=21, total=402
measure + layout=0, delay=357, anim=2, touch=0, draw=0, total=362
measure + layout=19, delay=4, anim=0, touch=0, draw=3, total=39
measure + layout=0, delay=16, anim=0, touch=0, draw=0, total=40
measure + layout
时间从 370 ms 缩短到 330 ms,可喜可贺~~
不同的 ViewGroup,不同的 measure + layout 耗时
想到的第二个优化方案是:“替换表项的根布局”
表头和表头 item 的布局都是用了ConstraintLayout
,是不是因为它太复杂了,所以导致measure + layout
耗时过长?
带着怀疑,我把所有的ConstraintLayout
换成了FrameLayout
,界面就变成了这样:
所有子控件都聚拢在一点,再瞄一眼性能日志:
measure + layout=272, delay=40, anim=0, touch=0, draw=15, total=312
measure + layout=0, delay=300, anim=2, touch=0, draw=0, total=332
measure + layout=9, delay=7, anim=0, touch=0, draw=2, total=21
measure + layout=7, delay=4, anim=0, touch=0, draw=1, total=55
measure + layout=0, delay=0, anim=0, touch=0, draw=0, total=41
令人惊喜的是measure + layout
时间从 330 ms 缩短到了 272 ms。
看来表项根布局的复杂程度的确可以影响到列表的加载性能,而且列表会放大这个性能差距,因为 n 个表项就会进行 n 次
measure + layout
那就用最最简单的FrameLayout
来布局吧~。通过leftMargin
和topMargin
来定位表项中的每一个子控件。我对着 UI 设计图,读取了每个子控件相对于父控件的左边距和上边距,然后用FrameLayout
重写了表头 item。
但当我把 demo 在不同手机上运行之后,发现这个方案有缺陷,虽然已经使用了 dp 而不是像素值,但依然无法很好地解决多屏幕适配的问题:
“粉丝数”根据左边距和上边距来确定相对于父控件的位置,不同的手机屏宽度不同,所以适配效果很差。
可能这就是相对布局存在的原因,但RelativeLayout
也不是省油的灯。有没有别的更简单的方法?
我想到了百分比布局,还是基于左边距和上边距,但这次不使用 dp 值,而是用相对于父控件的百分比,不就能完美解决这个问题吗?
立马搜索了一下,遗憾的发现PercentFrameLayout
已经被弃用。。。
那就自己手写一个:
// 自定义百分比布局
class PercentLayout
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0)
: ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 测量所有子控件
measureChildren(widthMeasureSpec, heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// 父控件宽
val parentWidth = right - left
// 父控件高
val parentHeight = bottom - top
// 遍历子控件逐个定位它们
(0 until childCount).map { getChildAt(it) }.forEach { child ->
val lp = child.layoutParams as LayoutParam
// 计算子控件 left 值(相对于父控件宽)
val childLeft = (parentWidth * lp.leftPercent).toInt()
// 计算子控件 top 值(相对于父控件高)
val childTop = (parentHeight * lp.topPercent).toInt()
// 定位子控件
child.layout(childLeft, childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight)
}
}
// 自定义的布局参数, 新增了两个属性分别是左百分比和上百分比
class LayoutParam(source: ViewGroup.LayoutParams?) : ViewGroup.LayoutParams(source) {
var leftPercent: Float = 0f
var topPercent: Float = 0f
}
}
百分比布局的编码很简单,只需要两步:先测量所有子控件,然后按需要定位所有子控件。其中测量孩子使用ViewGroup.measureChildren()
就完成了。布局孩子得先计算出父控件的宽高,然后与子控件的百分比相乘就得到了相对于父控件的位置,最后调用View.layout()
来定位子控件。
运行一下 demo,效果理想~~
运用相同的思路重构了一下表体 item 。过程中发现了一个问题:并不是所有控件都可以相对于父控件来布局。
比如下面这个场景:
表项数据是服务器返回的,文字长度是可变的,“等烟雨来,就是不来”后面与它垂直对齐的图片就无法相对于父控件布局。
所以PrecentLayout
不得不也引入相对布局的概念,但也不需要像ConstraintLayout
那样复杂,一个简化版的百分比相对布局如下:
// 自定义百分比相对布局
class PercentLayout
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0)
: ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
// 记录所有子控件及它 id 的 map
private val childMap = SparseArray<View>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 测量所有子控件
measureChildren(widthMeasureSpec, heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val parentWidth = right - left
val parentHeight = bottom - top
(0 until childCount).map { getChildAt(it) }.forEach { child ->
val lp = child.layoutParams as LayoutParam
// 计算子控件 left 值
val childLeft = getChildLeft(lp, parentWidth, child)
// 计算子控件 top 值
val childTop = getChildTop(lp, parentHeight, child)
// 布局子控件
child.layout(childLeft, childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight)
}
}
// 根据不同的情况计算相对于父控件上边的距离
private fun getChildTop(lp: LayoutParam, parentHeight: Int, child: View): Int {
val parentId = parent_id.toLayoutId()
return when {
lp.topPercent != -1f -> (parentHeight * lp.topPercent).toInt()
lp.centerVerticalOf != -1 -> {
if (lp.centerVerticalOf == parentId) {
(parentHeight - child.measuredHeight) / 2
} else {
(childMap.get(lp.centerVerticalOf)?.let { it.top + (it.bottom - it.top) / 2 } ?: 0) - child.measuredHeight / 2
}
}
lp.topToBottomOf != -1 -> {
val b = if (lp.topToBottomOf == parentId) bottom else childMap.get(lp.topToBottomOf)?.bottom ?: 0
(b + lp.topMargin)
}
lp.topToTopOf != -1 -> {
val t = if (lp.topToTopOf == parentId) top else childMap.get(lp.topToTopOf)?.top ?: 0
(t + lp.topMargin)
}
lp.bottomToTopOf != -1 -> {
val t = if (lp.bottomToTopOf == parentId) top else childMap.get(lp.bottomToTopOf)?.top ?: 0
(t - lp.bottomMargin) - child.measuredHeight
}
lp.bottomToBottomOf != -1 -> {
val b = if (lp.bottomToBottomOf == parentId) bottom else childMap.get(lp.bottomToBottomOf)?.bottom ?: 0
(b - lp.bottomMargin) - child.measuredHeight
}
else -> 0
}
}
// 根据不同的情况计算相对于父控件左边的距离
private fun getChildLeft(lp: LayoutParam, parentWidth: Int, child: View): Int {
val parentId = parent_id.toLayoutId()
return when {
lp.leftPercent != -1f -> (parentWidth * lp.leftPercent).toInt()
lp.centerHorizontalOf != -1 -> {
if (lp.centerHorizontalOf == parentId) {
(parentWidth - child.measuredWidth) / 2
} else {
(childMap.get(lp.centerHorizontalOf)?.let { it.left + (it.right - it.left) / 2 } ?: 0) - child.measuredWidth / 2
}
}
lp.startToEndOf != -1 -> {
val r = if (lp.startToEndOf == parentId) right else childMap.get(lp.startToEndOf)?.right ?: 0
(r + lp.marginStart)
}
lp.startToStartOf != -1 -> {
val l = if (lp.startToStartOf == parentId) left else childMap.get(lp.startToStartOf)?.left ?: 0
(l + lp.marginStart)
}
lp.endToStartOf != -1 -> {
val l = if (lp.endToStartOf == parentId) left else childMap.get(lp.endToStartOf)?.left ?: 0
(l - lp.marginEnd) - child.measuredWidth
}
lp.endToEndOf != -1 -> {
val r = if (lp.endToEndOf == parentId) right else childMap.get(lp.endToEndOf)?.right ?: 0
(r - lp.marginEnd) - child.measuredWidth
}
else -> 0
}
}
// 当有新子控件加入时,将其 id 和引用存入 map
override fun onViewAdded(child: View?) {
super.onViewAdded(child)
child?.let { childMap.put(it.id, it) }
}
// 当有新子控件被移出时,将其 id 和引用移出 map
override fun onViewRemoved(child: View?) {
super.onViewRemoved(child)
child?.let { childMap.remove(it.id) }
}
// 自定义布局参数
class LayoutParam(source: ViewGroup.LayoutParams?) : MarginLayoutParams(source) {
// 横向属性
var leftPercent: Float = -1f
var startToStartOf: Int = -1
var startToEndOf: Int = -1
var endToEndOf: Int = -1
var endToStartOf: Int = -1
var centerHorizontalOf: Int = -1
// 纵向属性
var topPercent: Float = -1f
var topToTopOf: Int = -1
var topToBottomOf: Int = -1
var bottomToTopOf: Int = -1
var bottomToBottomOf: Int = -1
var centerVerticalOf: Int = -1
}
}
PercentLayout
使用了SparseArray
来存储子控件 id 和子控件引用的对应关系。其实只要拿到了View
就可以拿到它的 id,为啥还要特意将这些信息存储在一个 map 结构中?因为想用空间换一点时间,否则每次都得遍历所有子控件。使用SparseArray
而不是HashMap
也是出于节约内存的考虑,相对而言,它有更好的内存效率,详细分析可以点击内存优化:充满矛盾的SparseArray。为
PercentLayout
新增了一系列相对布局属性,这些属性的语义和ConstraintLayout
中的一样。但有两个比较特殊的:centerHorizontalOf
表示相对于某个控件水平对齐,centerVerticalOf
表示相对于某个控件垂直对齐。这一系列相对布局属性存在互斥关系,他们分为两组,一组横向,一组纵向(详见代码注释)。一个控件只能拥有一个横向属性和一个纵向属性。
getChildLeft()
和getChildTop
分别遍历所有的横向和纵向属性,根据不同的相对位置采取不同的计算方法,以确定子控件相对于父控件的 left 和 top。
然后就可以像这样构建表体 item 的布局:
PercentLayout {
layout_width = match_parent
layout_height = 35
background_color = "#ffffff"
TextView {
layout_id = "tvRank"
layout_width = 18
layout_height = wrap_content
textSize = 14f
textColor = "#9DA4AD"
left_percent = 0.08f // 相对于父控件左边的百分比
center_vertical_of_percent = parent_id //相对于父控件垂直居中
}
ImageView {
layout_id = "ivAvatar"
layout_width = 20
layout_height = 20
scaleType = scale_center_crop
center_vertical_of_percent = parent_id // 相对于父控件垂直居中
left_percent = 0.15f // 相对于父控件左边的百分比
}
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 11f
textColor = "#3F4658"
gravity = gravity_center
maxLines = 1
includeFontPadding = false
start_to_end_of_percent = "ivAvatar" // 位于 ivAvatar 控件的右边
top_to_top_of_percent = "ivAvatar" // 与 ivAvatar 控件上边对齐
margin_start = 5
}
TextView {
layout_id = "tvTag"
layout_width = wrap_content
layout_height = wrap_content
textSize = 8f
textColor = "#ffffff"
text = "save"
gravity = gravity_center
padding_vertical = 1
includeFontPadding = false
padding_horizontal = 2
shape = shape {
corner_radius = 4
solid_color = "#8cc8c8c8"
}
start_to_start_of_percent = "tvName" // 与 tvName 控件左边对齐
top_to_bottom_of_percent = "tvName" // 在 tvName 控件的下面
}
ImageView {
layout_id = "ivLevel"
layout_width = 10
layout_height = 10
scaleType = scale_fit_xy
center_vertical_of_percent = "tvName" // 与 tvName 控件垂直对齐
start_to_end_of_percent = "tvName" // 在 tvName 控件的后面
margin_start = 5
}
TextView {
layout_id = "tvLevel"
layout_width = wrap_content
layout_height = wrap_content
textSize = 7f
textColor = "#ffffff"
gravity = gravity_center
padding_horizontal = 2
shape = shape {
gradient_colors = listOf("#FFC39E", "#FFC39E")
orientation = gradient_left_right
corner_radius = 20
}
center_vertical_of_percent = "tvName" // 与 tvName 控件垂直对齐
start_to_end_of_percent = "ivLevel" // 在 ivLevel 控件后面
margin_start = 5
}
TextView {
layout_id = "tvCount"
layout_width = wrap_content
layout_height = wrap_content
textSize = 14f
textColor = "#3F4658"
gravity = gravity_center
center_vertical_of_percent = parent_id // 相对于父控件居中
end_to_end_of_percent = parent_id // 在父控件尾部
margin_end = 20
}
}
把 demo 跑起来,measure + layout 的耗时如下:
measure + layout=288, delay=39, anim=0, touch=0, draw=20, total=350
measure + layout=0, delay=307, anim=4, touch=0, draw=0, total=314
measure + layout=15, delay=9, anim=0, touch=0, draw=4, total=31
measure + layout=0, delay=14, anim=0, touch=0, draw=0, total=27
measure + layout
用了 288 ms,虽然相对于FrameLayout
多了十几毫秒,但是和ConstraintLayout
的 330 ms 相比还是有不小的提升。
measure + layout
耗时从最开始的 370 ms 经过两次优化,分别是弃用 xml和替换表项根布局,缩减到 288 ms,有了 22% 的性能提升。但是离“耗时减半”还有点距离。限于篇幅原因,后续的优化详解放到下一篇继续讲解。
Talk is cheap, show me the code
相对百分比布局 + layout DSL 代码地址(https://github.com/wisdomtl/Layout_DSL) 代理模式解耦RecyclerView
代码地址(https://github.com/wisdomtl/VarietyAdapter)
---END---
更文不易,点个“在看”支持一下👇